package modlgen

import (
	"bytes"
	l4g "code.google.com/p/log4go"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
)

type AttributeDiffBase struct {
	TypeId      uint32
	AttributeId uint32
}

func (d AttributeDiffBase) GetAttributeId() (typeId, attributeId uint32) {
	return d.TypeId, d.AttributeId
}

type attrList []mdoAttrDef

func (l attrList) Len() int           { return len(l) }
func (l attrList) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
func (l attrList) Less(i, j int) bool { return l[i].attributeId < l[j].attributeId }

type mdoTypeDef struct {
	typeId            int
	name              string
	abstract          bool
	extendsName       string
	extends           *mdoTypeDef
	definedInFileName string
	definedOnLineNum  int
	attributeImports  []string
	attributeMap      map[int]mdoAttrDef
	attributes        attrList
	methodImports     []string
	abstractMethods   []string
	methods           []string
	fileReader        *fileReader
	imports           map[string]string
	persistent        bool
}

func newMdoTypeDef(fileReader *fileReader) *mdoTypeDef {
	typeDef := new(mdoTypeDef)
	typeDef.attributeImports = make([]string, 0)
	typeDef.attributeMap = make(map[int]mdoAttrDef)
	typeDef.methodImports = make([]string, 0)
	typeDef.abstractMethods = make([]string, 0)
	typeDef.methods = make([]string, 0)
	typeDef.fileReader = fileReader
	typeDef.definedInFileName = fileReader.filename
	typeDef.definedOnLineNum = fileReader.lineIdx
	l4g.Trace("Parsing type definition on line %d", typeDef.fileReader.lineIdx)
	typeDef.parseTypeDefHeader()
sectionLoop:
	for {
		fileReader.getNextContentLine()
		switch fileReader.currentContentLineParts[0] {
		case "attributes":
			typeDef.parseAttributeBlock()
			sort.Strings(typeDef.attributeImports)
			typeDef.attributes = make(attrList, len(typeDef.attributeMap))
			i := 0
			for _, v := range typeDef.attributeMap {
				typeDef.attributes[i] = v
				i++
			}
			sort.Sort(typeDef.attributes)

		case "methods":
			typeDef.parseMethodBlock()
			sort.Strings(typeDef.methodImports)
			sort.Strings(typeDef.abstractMethods)
			sort.Strings(typeDef.methods)

		case "}":
			break sectionLoop
		}
	}
	typeDef.fileReader = nil
	return typeDef
}

func (typeDef *mdoTypeDef) parseTypeDefHeader() {
	l4g.Trace("Parsing %s as type def header from line %d of file %s", typeDef.fileReader.currentContentLineParts, typeDef.fileReader.lineIdx, typeDef.fileReader.filename)
	if len(typeDef.fileReader.currentContentLineParts) < 2 {
		panic(fmt.Sprintf("Missing type number, line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	var err error
	typeDef.typeId, err = strconv.Atoi(typeDef.fileReader.currentContentLineParts[1])
	if err != nil {
		panic(fmt.Sprintf("Expecting integer type number, found \"%s\", line %d of file %s", typeDef.fileReader.currentContentLineParts[1], typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}

	if len(typeDef.fileReader.currentContentLineParts) < 3 {
		panic(fmt.Sprintf("Missing type name, line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	typeDef.name = typeDef.fileReader.currentContentLineParts[2]

	if len(typeDef.fileReader.currentContentLineParts) < 4 {
		panic(fmt.Sprintf("Missing abstract/concrete classification, line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	if typeDef.fileReader.currentContentLineParts[3] == "abstract" {
		typeDef.abstract = true
	} else if typeDef.fileReader.currentContentLineParts[3] == "concrete" {
		typeDef.abstract = false
	} else {
		panic(fmt.Sprintf("Expecting \"abstract\" or \"concrete\", found \"%s\", line %d of file %s", typeDef.fileReader.currentContentLineParts[3], typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}

	if typeDef.fileReader.currentContentLineParts[len(typeDef.fileReader.currentContentLineParts)-1] != "{" {
		panic(fmt.Sprintf("Missing \"{\", line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}

	if len(typeDef.fileReader.currentContentLineParts) == 5 {
		return
	}

	if typeDef.fileReader.currentContentLineParts[4] != "extends" {
		panic(fmt.Sprintf("Expecting \"extends\", found \"%s\", line %d of file %s", typeDef.fileReader.currentContentLineParts[5], typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	if len(typeDef.fileReader.currentContentLineParts) < 7 {
		panic(fmt.Sprintf("Missing extended type name, line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	typeDef.extendsName = typeDef.fileReader.currentContentLineParts[5]
	return
}

func (typeDef *mdoTypeDef) parseAttributeBlock() {
	l4g.Trace("Parsing %s as attribute block header from line %d of file %s", typeDef.fileReader.currentContentLineParts, typeDef.fileReader.lineIdx, typeDef.fileReader.filename)
	if typeDef.fileReader.currentContentLineParts[len(typeDef.fileReader.currentContentLineParts)-1] != "{" {
		panic(fmt.Sprintf("Missing \"{\", line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	for {
		typeDef.fileReader.getNextContentLine()
		switch typeDef.fileReader.currentContentLineParts[0] {
		case "}":
			return
		default:
			attr := newMdoAttrDef(typeDef)
			if existingAttr, present := typeDef.attributeMap[attr.attributeId]; present {
				panic(fmt.Sprintf("Attribute id %d redefined on line %d of file %s. Previous definition on line %d", attr.attributeId, attr.definedOnLineNum, typeDef.fileReader.filename, existingAttr.definedOnLineNum))
			}
			typeDef.attributeMap[attr.attributeId] = attr
			switch attr.attributeClass {
			case MDO_ATTR_CLASS_TIME:
				typeDef.attributeImports = append(typeDef.attributeImports, "time")
			case MDO_ATTR_CLASS_ARRAY, MDO_ATTR_CLASS_SLICE:
				if attr.collectionClass == MDO_ATTR_CLASS_TIME {
					typeDef.attributeImports = append(typeDef.attributeImports, "time")
				}
			}
		}
	}
}

func (typeDef *mdoTypeDef) parseMethodBlock() {
	l4g.Trace("Parsing %s as method block header from line %d of file %s", typeDef.fileReader.currentContentLineParts, typeDef.fileReader.lineIdx, typeDef.fileReader.filename)
	if typeDef.fileReader.currentContentLineParts[len(typeDef.fileReader.currentContentLineParts)-1] != "{" {
		panic(fmt.Sprintf("Missing \"{\", line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	for {
		typeDef.fileReader.getNextContentLine()
		switch typeDef.fileReader.currentContentLineParts[0] {
		case "}":
			return
		case "import":
			typeDef.addImport(&typeDef.methodImports)
		default:
			if typeDef.fileReader.currentContentLineParts[len(typeDef.fileReader.currentContentLineParts)-1] == "{" {
				typeDef.parseMethod()
			} else {
				typeDef.abstractMethods = append(typeDef.abstractMethods, typeDef.fileReader.currentContentLine)
			}
		}
	}
}

func (typeDef *mdoTypeDef) addImport(imports *[]string) {
	if len(typeDef.fileReader.currentContentLineParts) != 2 {
		panic(fmt.Sprintf("Invalid import definition, line %d of file %s", typeDef.fileReader.lineIdx, typeDef.fileReader.filename))
	}
	pkg := typeDef.fileReader.currentContentLineParts[1]
	pkg = strings.Replace(pkg, "\"", "", -1)
	*imports = append(*imports, pkg)
}

func (typeDef *mdoTypeDef) parseMethod() {
	l4g.Trace("Parsing %s as method start from line %d of file %s", typeDef.fileReader.currentContentLineParts, typeDef.fileReader.lineIdx, typeDef.fileReader.filename)
	indentLevel := 0
	var buffer bytes.Buffer
	for {
		if len(typeDef.fileReader.currentContentLineParts) == 1 && typeDef.fileReader.currentContentLineParts[0] == "}" {
			indentLevel--
		}
		for i := 0; i < indentLevel; i++ {
			buffer.WriteString("    ")
		}
		buffer.WriteString(typeDef.fileReader.currentContentLine)
		buffer.WriteString("\n")
		if typeDef.fileReader.currentContentLineParts[len(typeDef.fileReader.currentContentLineParts)-1] == "{" {
			indentLevel++
		}
		if indentLevel == 0 {
			typeDef.methods = append(typeDef.methods, buffer.String())
			l4g.Debug("   Found method definition: \n%s", buffer.String())
			return
		}
		typeDef.fileReader.getNextLine()
	}
}

func (typeDef *mdoTypeDef) checkHierarchyForDuplicateAttributeNames() {
	if typeDef.extends == nil {
		return
	}
	for _, attrDef := range typeDef.attributes {
		dupType, dupAttr := typeDef.extends.checkHierarchyForDuplicateAttributeName(attrDef.name)
		if dupType != nil && dupAttr != nil {
			panic(fmt.Sprintf("Attribute %s in type %s on line %d of file %s collides with attribute %s in type %s on line %d of file %s",
				attrDef.name, typeDef.name, typeDef.definedOnLineNum, typeDef.definedInFileName,
				dupAttr.name, dupType.name, dupType.definedOnLineNum, dupType.definedInFileName))
		}
	}
}

func (typeDef *mdoTypeDef) checkHierarchyForDuplicateAttributeName(attributeName string) (*mdoTypeDef, *mdoAttrDef) {
	if typeDef.extends != nil {
		typeDef.extends.checkHierarchyForDuplicateAttributeName(attributeName)
	}
	for _, attrDef := range typeDef.attributes {
		if attrDef.name == attributeName {
			return typeDef, &attrDef
		}
	}
	return nil, nil
}

type methodDef struct {
	typeDef         *mdoTypeDef
	method          string
	methodName      string
	methodSignature string
}

func (typeDef *mdoTypeDef) resolveMethodHierarchy() {
	resolvedMethods := make(map[string]methodDef, 0)
	typeDef.resolveMethods(resolvedMethods)
	typeDef.methods = make([]string, 0)
	for _, methodDef := range resolvedMethods {
		typeDef.methods = append(typeDef.methods, methodDef.method)
	}
	sort.Strings(typeDef.methods)
}

func (typeDef *mdoTypeDef) resolveMethods(resolvedMethods map[string]methodDef) {
	for _, method := range typeDef.methods {
		typeDef.resolveMethod(method, resolvedMethods)
	}
	if typeDef.extends != nil {
		typeDef.extends.resolveMethods(resolvedMethods)
	}
}

func (typeDef *mdoTypeDef) resolveMethod(method string, resolvedMethods map[string]methodDef) {
	methodName, methodSignature := getMethodSignature(method)
	if strings.Contains(method, "##super##") {
		superName := findSuperName(methodName, typeDef.extends)
		if superName == "" {
			panic(fmt.Sprintf("Couldn't find super method for %s in type %s defined on line %d in file %s", methodName, typeDef.name, typeDef.definedOnLineNum, typeDef.definedInFileName))
		}
		method = strings.Replace(method, "##super##", superName, -1)
	}
	if existingMethodDef, present := resolvedMethods[methodName]; present {
		if existingMethodDef.methodSignature != methodSignature {
			panic(fmt.Sprintf("Name clash for method %s with differing signatures: type %s and type %s", typeDef.name, existingMethodDef.typeDef.name))
		}
		newMethodName := fmt.Sprint(strings.ToLower(typeDef.name), "_", methodName)
		resolvedMethods[newMethodName] = methodDef{typeDef, strings.Replace(method, methodName, newMethodName, -1), newMethodName, strings.Replace(methodSignature, methodName, newMethodName, -1)}
	} else {
		resolvedMethods[methodName] = methodDef{typeDef, method, methodName, methodSignature}
	}
}

func getMethodSignature(method string) (methodName, methodSignature string) {
	methodLines := strings.Split(method, "\n")
	methodSignature = strings.Join(strings.Fields(methodLines[0]), " ")
	methodName = methodSignature[:strings.Index(methodSignature, "(")]
	return
}

func findSuperName(methodName string, typeDef *mdoTypeDef) string {
	if typeDef == nil {
		return ""
	}
	for _, method := range typeDef.methods {
		testMethodName, _ := getMethodSignature(method)
		if testMethodName == methodName {
			return fmt.Sprint("this.", strings.ToLower(typeDef.name), "_", methodName)
		}
	}
	return findSuperName(methodName, typeDef.extends)
}

func (typeDef *mdoTypeDef) generate(destPath, pkgName string) {
	tmpFileName := filepath.Join(destPath, fmt.Sprintf("%s.go.tmp", typeDef.name))
	destFileName := strings.Replace(tmpFileName, ".go.tmp", ".go", -1)
	typeDef.imports = make(map[string]string, 20)
	typeDef.generateTmp(tmpFileName, pkgName)
	formatTmp(tmpFileName)
	if different, destExists := isDifferent(destFileName, tmpFileName); different {
		if destExists {
			l4g.Info("Replacing file %s", destFileName)
		} else {
			l4g.Info("Creating new file %s", destFileName)
		}
		replaceFile(destFileName, tmpFileName)
	} else {
		l4g.Info("Not replacing file %s, since contents haven't changed", destFileName)
		removeFile(tmpFileName)
	}
}

func (typeDef *mdoTypeDef) generateTmp(fileName, pkgName string) {
	file, err := os.Create(fileName)
	if err != nil {
		panic(fmt.Sprintf("Couldn't open file %s, %s", fileName, err))
	}
	defer func() {
		if err := file.Close(); err != nil {
			panic(fmt.Sprintf("Couldn't close file %s. %s", fileName, err))
		}
	}()
	w := newWriter(file)
	w.Printf("package generated_%s\n\n", pkgName)
	w.Printf("import (\n")
	if !typeDef.abstract {
		w.Printf("    . \"code.google.com/p/go-modl\"\n")
		typeDef.generateAttributeImports(typeDef, w)
	}
	typeDef.generateMethodImports(typeDef, w)
	w.Printf(")\n\n")
	w.Printf("const (\n")
	w.Printf("    %sTypeId = %d\n", typeDef.name, typeDef.typeId)
	w.Printf(")\n\n")

	if len(typeDef.attributes) > 0 {
		w.Printf("const (\n")
		for _, attrDef := range typeDef.attributes {
			w.Printf("    %sAttributeId_%s = %d\n", typeDef.name, attrDef.name, attrDef.attributeId)
		}
		w.Printf(")\n\n")
	}
	if typeDef.abstract {
		typeDef.generateInterface(w)
	} else {
		typeDef.generateFull(w)
	}
	if err = w.Flush(); err != nil {
		panic(fmt.Sprintf("Couldn't flush output to file %s. %s", fileName, err))
	}
}

func formatTmp(fileName string) {
	cmd := exec.Command("gofmt", "-w", fileName)
	var errorOutput bytes.Buffer
	cmd.Stderr = &errorOutput
	err := cmd.Run()
	if err != nil {
		panic(fmt.Sprintf("Couldn't format file %s... gofmt output:\n%s", fileName, errorOutput.String()))
	}
}

func (typeDef *mdoTypeDef) generateAttributeImports(rootTypeDef *mdoTypeDef, w *writer) {
	if typeDef.extends != nil {
		typeDef.extends.generateAttributeImports(rootTypeDef, w)
	}
	for _, pkg := range typeDef.attributeImports {
		if _, exists := rootTypeDef.imports[pkg]; !exists {
			w.Printf("    \"%s\"\n", pkg)
			rootTypeDef.imports[pkg] = pkg
		}
	}
}

func (typeDef *mdoTypeDef) generateMethodImports(rootTypeDef *mdoTypeDef, w *writer) {
	if typeDef.extends != nil {
		typeDef.extends.generateMethodImports(rootTypeDef, w)
	}
	for _, pkg := range typeDef.methodImports {
		if _, exists := rootTypeDef.imports[pkg]; !exists {
			w.Printf("    \"%s\"\n", pkg)
			rootTypeDef.imports[pkg] = pkg
		}
	}
	if !typeDef.abstract {
		for _, attrDef := range typeDef.attributes {
			if attrDef.attributeClass == MDO_ATTR_CLASS_ARRAY || attrDef.attributeClass == MDO_ATTR_CLASS_SLICE {
				if _, exists := rootTypeDef.imports["fmt"]; !exists {
					w.Printf("    \"fmt\"\n")
					rootTypeDef.imports["fmt"] = "fmt"
				}
				if _, exists := rootTypeDef.imports["github.com/davecgh/go-spew/spew"]; !exists {
					w.Printf("    \"github.com/davecgh/go-spew/spew\"\n")
					rootTypeDef.imports["github.com/davecgh/go-spew/spew"] = "github.com/davecgh/go-spew/spew"
				}
				break
			}
		}
	}
}

func (typeDef *mdoTypeDef) generateInterface(w *writer) {
	w.Printf("type %s interface {\n", typeDef.name)
	typeDef.generateInterfaceMethods(w)
	w.Printf("}\n\n")
}

func (typeDef *mdoTypeDef) generateInterfaceMethods(w *writer) {
	if typeDef.extends != nil {
		typeDef.extends.generateInterfaceMethods(w)
	}
	for _, method := range typeDef.abstractMethods {
		w.Printf("    %s\n", method)
	}
	for _, method := range typeDef.methods {
		methodLines := strings.Split(method, "\n")
		w.Printf("    ")
		w.Printf(strings.Replace(methodLines[0], "{", "\n", -1))
	}
}

func (typeDef *mdoTypeDef) generateFull(w *writer) {

	w.Printf("type %s struct {\n", typeDef.name)
	typeDef.persistent = false
	typeDef.generateFields(w)
	if typeDef.persistent {
		w.Printf("    SeqNum uint32\n")
	}
	w.Printf("}\n\n")
	typeDef.generateMethods(typeDef, w)
	typeDef.generateMDOMethods(w)
}

func (typeDef *mdoTypeDef) generateFields(w *writer) {
	if typeDef.extends != nil {
		typeDef.extends.generateFields(w)
	}
	for _, attrDef := range typeDef.attributes {
		if attrDef.persistenceType == "persistent" {
			typeDef.persistent = true
		}
		w.Printf("    %s %s\n", attrDef.name, attrDef.attributeTypeFull)
	}
}

func (typeDef *mdoTypeDef) generateMethods(topLevelTypeDef *mdoTypeDef, w *writer) {
	for _, method := range typeDef.methods {
		w.Printf("func (this *%s) %s\n", topLevelTypeDef.name, method)
	}
}

func (typeDef *mdoTypeDef) generateMDOMethods(w *writer) {
	typeDef.generateEquals(w)
	typeDef.generateMDODiff(w)
	typeDef.generateApply(w)
	typeDef.generateMDOSerialize(w)
	typeDef.generateMDODeserialize(w)
}

func (typeDef *mdoTypeDef) generateEquals(w *writer) {
	w.Printf("func (o1 *%s) Equals(other interface{}) bool {\n", typeDef.name)
	w.Printf("    switch o2 := other.(type) {\n")
	w.Printf("    case *%s:\n", typeDef.name)
	w.Printf("        return o1.equals(o2)\n")
	w.Printf("    case %s:\n", typeDef.name)
	w.Printf("        return o1.equals(&o2)\n")
	w.Printf("    }\n")
	w.Printf("    return false\n")
	w.Printf("}\n\n")

	w.Printf("func (o1 *%s) equals(o2 *%s) bool {\n", typeDef.name, typeDef.name)
	typeDef.generateAttrEquals(w)
	w.Printf("    return true\n")
	w.Printf("}\n\n")
}

func (typeDef *mdoTypeDef) generateAttrEquals(w *writer) {
	if typeDef.extends != nil {
		typeDef.extends.generateAttrEquals(w)
	}
	for _, attrDef := range typeDef.attributes {
		if attrDef.persistenceType == "ephemeral" {
			continue
		}
		switch attrDef.attributeClass {
		case MDO_ATTR_CLASS_BASIC:
			w.Printf("        if o1.%s != o2.%s {\n", attrDef.name, attrDef.name)
			w.Printf("            return false\n")
			w.Printf("        }\n")
		case MDO_ATTR_CLASS_TIME:
			w.Printf("        if !o1.%s.Equal(o2.%s) {\n", attrDef.name, attrDef.name)
			w.Printf("            return false\n")
			w.Printf("        }\n")
		case MDO_ATTR_CLASS_ARRAY, MDO_ATTR_CLASS_SLICE:
			w.Printf("        if len(o1.%s) != len(o2.%s) {\n", attrDef.name, attrDef.name)
			w.Printf("            return false\n")
			w.Printf("        }\n")
			w.Printf("        for idx, newVal := range o1.%s {\n", attrDef.name)
			switch attrDef.collectionClass {
			case MDO_ATTR_CLASS_BASIC, MDO_ATTR_CLASS_TIME:
				w.Printf("            if oldVal := o2.%s[idx]; oldVal != newVal {\n", attrDef.name)
				w.Printf("                return false\n")
				w.Printf("           }\n")
			case MDO_ATTR_CLASS_STRUCT:
				w.Printf("            if oldVal := o2.%s[idx]; !oldVal.Equals(newVal) {\n", attrDef.name)
				w.Printf("                return false\n")
				w.Printf("           }\n")
			default:
				panic(fmt.Sprintf("Don't know what to do for collection class %d, attribute %s[%d] in type %s defined on line %d in file %s", attrDef.collectionClass, attrDef.name, attrDef.attributeId, typeDef.name, typeDef.definedOnLineNum, typeDef.definedInFileName))
			}
			w.Printf("        }\n")
		case MDO_ATTR_CLASS_STRUCT:
			w.Printf("        if !o1.%s.Equals(&o2.%s) {\n", attrDef.name, attrDef.name)
			w.Printf("            return false\n")
			w.Printf("        }\n")
		}
	}
}

func (typeDef *mdoTypeDef) generateMDODiff(w *writer) {
	w.Printf("func (newObj *%s) Diff(oldObj *%s) (diff MDODiff) {\n", typeDef.name, typeDef.name)
	typeDef.generateMDOAttrDiffs(w)
	w.Printf("    return diff\n")
	w.Printf("}\n\n")
}

func (typeDef *mdoTypeDef) generateMDOAttrDiffs(w *writer) {
	if typeDef.extends != nil {
		typeDef.extends.generateMDOAttrDiffs(w)
	}
	for _, attrDef := range typeDef.attributes {
		if attrDef.persistenceType == "ephemeral" {
			continue
		}
		switch attrDef.attributeClass {
		case MDO_ATTR_CLASS_BASIC:
			w.Printf("    if newObj.%s != oldObj.%s {\n", attrDef.name, attrDef.name)
			w.Printf("        diff.Add(New%sDiff(%sTypeId, %sAttributeId_%s, %t, oldObj.%s, newObj.%s))\n", strings.Title(attrDef.attributeTypeShort), typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent", attrDef.name, attrDef.name)
			w.Printf("    }\n")
		case MDO_ATTR_CLASS_TIME:
			w.Printf("    if !newObj.%s.Equal(oldObj.%s) {\n", attrDef.name, attrDef.name)
			w.Printf("        diff.Add(NewTimeDiff(%sTypeId, %sAttributeId_%s, %t, oldObj.%s, newObj.%s))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent", attrDef.name, attrDef.name)
			w.Printf("    }\n")
		case MDO_ATTR_CLASS_ARRAY:
			w.Printf("    for idx, newVal := range newObj.%s {\n", attrDef.name)
			switch attrDef.collectionClass {
			case MDO_ATTR_CLASS_BASIC, MDO_ATTR_CLASS_TIME:
				w.Printf("        if oldVal := oldObj.%s[idx]; oldVal != newVal {\n", attrDef.name)
				w.Printf("            diff.Add(NewSliceDiff(%sTypeId, %sAttributeId_%s, %t, uint32(idx), DiffOper_modify, New%sSliceModification(oldVal, newVal)))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent", strings.Title(attrDef.collectionTypeShort))
			case MDO_ATTR_CLASS_STRUCT:
				w.Printf("        if oldVal := oldObj.%s[idx]; !oldVal.Equals(newVal) {\n", attrDef.name)
				w.Printf("            diff.Add(NewSliceDiff(%sTypeId, %sAttributeId_%s, %t, uint32(idx), DiffOper_modify, NewStructSliceModification(newVal.Diff(oldVal)))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent")
			default:
				panic(fmt.Sprintf("Don't know what to do for collection class %d, attribute %s[%d] in type %s defined on line %d in file %s", attrDef.collectionClass, attrDef.name, attrDef.attributeId, typeDef.name, typeDef.definedOnLineNum, typeDef.definedInFileName))
			}
			w.Printf("       }\n")
			w.Printf("    }\n")
		case MDO_ATTR_CLASS_SLICE:
			w.Printf("    for idx, newVal := range newObj.%s {\n", attrDef.name)
			w.Printf("        if idx >= len(oldObj.%s) {\n", attrDef.name)
			switch attrDef.collectionClass {
			case MDO_ATTR_CLASS_BASIC, MDO_ATTR_CLASS_TIME:
				w.Printf("            diff.Add(NewSliceDiff(%sTypeId, %sAttributeId_%s, %t, uint32(idx), DiffOper_add, New%sSliceAddition(newVal)))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent", strings.Title(attrDef.collectionTypeShort))
			case MDO_ATTR_CLASS_STRUCT:
				w.Printf("            diff.Add(NewSliceDiff(%sTypeId, %sAttributeId_%s, %t, uint32(idx), DiffOper_add, NewStructSliceAddition(newVal)))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent")
			default:
				panic(fmt.Sprintf("Don't know what to do for collection class %d, attribute %s[%d] in type %s defined on line %d in file %s", attrDef.collectionClass, attrDef.name, attrDef.attributeId, typeDef.name, typeDef.definedOnLineNum, typeDef.definedInFileName))
			}
			w.Printf("            continue\n")
			w.Printf("        }\n")
			switch attrDef.collectionClass {
			case MDO_ATTR_CLASS_BASIC, MDO_ATTR_CLASS_TIME:
				w.Printf("        if oldVal := oldObj.%s[idx]; oldVal != newVal {\n", attrDef.name)
				w.Printf("            diff.Add(NewSliceDiff(%sTypeId, %sAttributeId_%s, %t, uint32(idx), DiffOper_modify, New%sSliceModification(oldVal, newVal)))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent", strings.Title(attrDef.collectionTypeShort))
			case MDO_ATTR_CLASS_STRUCT:
				w.Printf("        if oldVal := oldObj.%s[idx]; !oldVal.Equals(newVal) {\n", attrDef.name)
				w.Printf("            diff.Add(NewSliceDiff(%sTypeId, %sAttributeId_%s, %t, uint32(idx), DiffOper_modify, NewStructSliceModification(newVal.Diff(oldVal))))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent")
			default:
				panic(fmt.Sprintf("Don't know what to do for collection class %d, attribute %s[%d] in type %s defined on line %d in file %s", attrDef.collectionClass, attrDef.name, attrDef.attributeId, typeDef.name, typeDef.definedOnLineNum, typeDef.definedInFileName))
			}
			w.Printf("            continue\n")
			w.Printf("        }\n")
			w.Printf("    }\n")
			w.Printf("    if len(oldObj.%s) > len(newObj.%s) {\n", attrDef.name, attrDef.name)
			w.Printf("       diff.Add(NewSliceDiff(%sTypeId, %sAttributeId_%s, %t, uint32(len(newObj.%s)), DiffOper_truncate, nil))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent", attrDef.name)
			w.Printf("    }\n")
		case MDO_ATTR_CLASS_STRUCT:
			w.Printf("    if !newObj.%s.Equals(&oldObj.%s) {\n", attrDef.name, attrDef.name)
			w.Printf("        sdiff := newObj.%s.Diff(&oldObj.%s)\n", attrDef.name, attrDef.name)
			w.Printf("        diff.Add(NewStructDiff(%sTypeId, %sAttributeId_%s, %t, &sdiff))\n", typeDef.name, typeDef.name, attrDef.name, attrDef.persistenceType == "persistent")
			w.Printf("    }\n")
		}
	}
}

func (typeDef *mdoTypeDef) generateApply(w *writer) {
	needDiffIdx := false
	for _, attrDef := range typeDef.attributes {
		switch attrDef.attributeClass {
		case MDO_ATTR_CLASS_ARRAY, MDO_ATTR_CLASS_SLICE:
			needDiffIdx = true
		}
	}
	w.Printf("func (obj *%s) Apply(diff *MDODiff) {\n", typeDef.name)
	if needDiffIdx {
		w.Printf("    for diffIdx, attributeDiff := range diff.AttributeDifferences {\n")
	} else {
		w.Printf("    for _, attributeDiff := range diff.AttributeDifferences {\n")
	}
	w.Printf("        typeId, attributeId := attributeDiff.GetAttributeId()\n")
	w.Printf("        switch typeId {\n")
	typeDef.generateAttrApply(w)
	w.Printf("        }\n")
	w.Printf("    }\n")
	w.Printf("}\n\n")
}

func (typeDef *mdoTypeDef) generateAttrApply(w *writer) {
	if typeDef.extends != nil {
		typeDef.extends.generateAttrApply(w)
	}
	w.Printf("        case %sTypeId:\n", typeDef.name)
	w.Printf("            switch attributeId {\n")
	for _, attrDef := range typeDef.attributes {
		if attrDef.persistenceType == "ephemeral" {
			continue
		}
		w.Printf("            case %sAttributeId_%s:\n", typeDef.name, attrDef.name)
		switch attrDef.attributeClass {
		case MDO_ATTR_CLASS_BASIC, MDO_ATTR_CLASS_TIME:
			w.Printf("                obj.%s = attributeDiff.(*%sDiff).NewValue\n\n", attrDef.name, strings.Title(attrDef.attributeTypeShort))
		case MDO_ATTR_CLASS_ARRAY:
			w.Printf("                %sDiff := attributeDiff.(*SliceDiff)\n", attrDef.name)
			w.Printf("                switch %sDiff.Oper {\n", attrDef.name)
			w.Printf("                case DiffOper_modify:\n")
			w.Printf("                    if uint32(len(obj.%s)) <= %sDiff.Idx {\n", attrDef.name, attrDef.name)
			w.Printf("                        panic(fmt.Sprintf(\"Invalid attempt to add to %s: Diff %%d in\\n%%s\\nExisting Object: %%s\", diffIdx, spew.Sdump(diff), spew.Sdump(*obj)))\n", attrDef.name)
			w.Printf("                    }\n")
			w.Printf("                    obj.%s[%sDiff.Idx] = %sDiff.ActualDiff.(*%sDiff).NewValue\n", attrDef.name, attrDef.name, attrDef.name, strings.Title(attrDef.collectionTypeShort))
			w.Printf("                default:\n")
			w.Printf("                    panic(fmt.Sprintf(\"Invalid diff operation: Diff %%d in %%s\", diffIdx, spew.Sdump(diff)))\n")
			w.Printf("                }\n\n")
		case MDO_ATTR_CLASS_SLICE:
			w.Printf("                %sDiff := attributeDiff.(*SliceDiff)\n", attrDef.name)
			w.Printf("                switch %sDiff.Oper {\n", attrDef.name)
			w.Printf("                case DiffOper_add:\n")
			w.Printf("                    obj.%s = append(obj.%s, %sDiff.ActualDiff.(*%sDiff).NewValue)\n", attrDef.name, attrDef.name, attrDef.name, strings.Title(attrDef.collectionTypeShort))
			w.Printf("                case DiffOper_modify:\n")
			w.Printf("                    if uint32(len(obj.%s)) <= %sDiff.Idx {\n", attrDef.name, attrDef.name)
			w.Printf("                        panic(fmt.Sprintf(\"Invalid attempt to modify %s: Diff %%d in\\n%%s\\nExisting Object: %%s\", diffIdx, spew.Sdump(diff), spew.Sdump(*obj)))\n", attrDef.name)
			w.Printf("                    }\n")
			w.Printf("                    obj.%s[%sDiff.Idx] = %sDiff.ActualDiff.(*%sDiff).NewValue\n", attrDef.name, attrDef.name, attrDef.name, strings.Title(attrDef.collectionTypeShort))
			w.Printf("                case DiffOper_truncate:\n")
			w.Printf("                    if uint32(len(obj.%s)) <= %sDiff.Idx {\n", attrDef.name, attrDef.name)
			w.Printf("                        panic(fmt.Sprintf(\"Invalid attempt to truncate %s: Diff %%d in\\n%%s\\nExisting Object: %%s\", diffIdx, spew.Sdump(diff), spew.Sdump(*obj)))\n", attrDef.name)
			w.Printf("                    }\n")
			w.Printf("                    {\n")
			w.Printf("                        temp := obj.%s\n", attrDef.name)
			w.Printf("                        obj.%s = make([]%s, %sDiff.Idx)\n", attrDef.name, attrDef.collectionTypeFull, attrDef.name)
			w.Printf("                        copy(obj.%s, temp)\n", attrDef.name)
			w.Printf("                    }\n")
			w.Printf("                default:\n")
			w.Printf("                    panic(fmt.Sprintf(\"Invalid diff operation: Diff %%d in %%s\", diffIdx, spew.Sdump(diff)))\n")
			w.Printf("                }\n\n")
		case MDO_ATTR_CLASS_STRUCT:
			w.Printf("                obj.%s.Apply(attributeDiff.(*StructDiff).Diff)\n\n", attrDef.name)
		}
	}
	w.Printf("            }\n\n")
}

func (typeDef *mdoTypeDef) generateMDOSerialize(w *writer) {
}

func (typeDef *mdoTypeDef) generateMDODeserialize(w *writer) {
}

func isDifferent(destFileName, srcFileName string) (different bool, destExists bool) {
	destBytes, err := ioutil.ReadFile(destFileName)
	if err != nil {
		if os.IsNotExist(err) {
			return true, false
		}
		if os.IsPermission(err) {
			panic(fmt.Sprintf("You do not have sufficient permissions to read file %s", destFileName))
		}
		panic(fmt.Sprintf("Couldn't read file %s, %s", destFileName, err))
	}
	srcBytes, err := ioutil.ReadFile(srcFileName)
	if err != nil {
		panic(fmt.Sprintf("Couldn't read file %s. Error: %s", srcFileName, err))
	}
	return !bytes.Equal(destBytes, srcBytes), true
}

func replaceFile(destFileName, srcFileName string) {
	if err := os.Rename(srcFileName, destFileName); err != nil {
		panic(fmt.Sprintf("Couldn't move file %s to file %s, %s", srcFileName, destFileName, err))
	}
}

func removeFile(fileName string) {
	if err := os.Remove(fileName); err != nil {
		panic(fmt.Sprintf("Couldn't remove file %s, %s", fileName, err))
	}
}
